package com.eunion.web.common.vo.jpa;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.ParameterExpression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.domain.Specification;

import com.eunion.core.LogUtil;
import com.google.gson.Gson;

/**
 * 用于生成较全面的查询jpa specification.
 * https://github.com/VovaOne/spring-data-specification-accurate-filter
 * http://www.programcreek.com/java-api-examples/index.php?api=javax.persistence.criteria.
 * CriteriaBuilder
 * http://stackoverflow.com/questions/29719321/combining-conditional-expressions-with-and-and-or-
 * predicates-using-the-jpa-c
 * https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications
 * 
 * 
 * @version 2015-9-12
 */
public class SpecificationFilter implements Specification {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private SpecVo specificationVo;

    public SpecificationFilter(String json) {
        try {
            specificationVo = new Gson().fromJson(json, SpecVo.class);
        } catch (com.google.gson.JsonSyntaxException e) {
            logger.error("{} json转换失败{}", json, e);
            specificationVo = null;
            LogUtil.get(this.getClass()).error("SpecVo json to pojo error ()", e.toString());
        }
    }

    public SpecificationFilter() {
    }

    public void setSpecVo(SpecVo vo) {
        this.specificationVo = vo;
    }

    @Override
    public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        List<Predicate> predicates = buildPredicates(root, criteriaQuery, criteriaBuilder);
        return predicates.size() > 1 ? criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()])) : predicates.get(0);
    }

    private List<Predicate> buildPredicates(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        // return conditions.stream().map(this::buildPredicate).collect(toList());
        List<Predicate> predicates = new ArrayList<Predicate>();
        for (SpecConVo condition : specificationVo.getSpecConVo()) {
            Predicate pre = buildPredicate(condition, root, criteriaQuery, criteriaBuilder);
            if (null != pre) {
                predicates.add(pre);
            }
        }
        // conditions.forEach(condition -> predicates.add(buildPredicate(condition, root,
        // criteriaQuery, criteriaBuilder)));
        return predicates;
    }

    /**
     * 构造查询条件.
     */
    public Predicate buildPredicate(SpecConVo con, Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        //Fall through from previous branch of the switch statement. if not have break;
        Predicate pre = null;
        switch (con.getCondition()) {
            case SpecDefine.CON_EQ:
                pre = buildPredicateToCriteriaEq(con, root, criteriaQuery, criteriaBuilder);
                break;
            case SpecDefine.CON_GE:
                pre = buildPredicateToCriteriaGe(con, root, criteriaQuery, criteriaBuilder);
                break;
            case SpecDefine.CON_GT:
                pre = buildPredicateToCriteriaGt(con, root, criteriaQuery, criteriaBuilder);
                break;
            case SpecDefine.CON_IN:
                pre = buildPredicateToCriteriaIn(con, root, criteriaQuery, criteriaBuilder);
                break;
            case SpecDefine.CON_LE:
                pre = buildPredicateToCriteriaLe(con, root, criteriaQuery, criteriaBuilder);
                break;
            case SpecDefine.CON_LK:
                pre = buildPredicateToCriteriaLk(con, root, criteriaQuery, criteriaBuilder);
                break;
            case SpecDefine.CON_NULL:
                pre = buildPredicateToCriteriaNULL(con, root, criteriaQuery, criteriaBuilder);
                break;
            default:
                pre = null;
                break;
        }
        return pre;
    }
    // http://stackoverflow.com/questions/29719321/combining-conditional-expressions-with-and-and-or-predicates-using-the-jpa-c
    // http://www.4byte.cn/question/346342/how-to-set-positional-named-parameters-dynamically-to-jpa-criteria-query.html

    private Predicate buildPredicateToCriteriaNULL(SpecConVo condition, Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        // https://dzone.com/articles/java-using-specification
        // DateTime now = new DateTime();
        // return criteriaBuilder.and(
        // criteriaBuilder.lessThan(root.get(condition.getField()), now),
        // criteriaBuilder.greaterThan(root.get(condition.getField()), now),
        // criteriaBuilder.isNull(root.get(condition.getField()))
        // );
        // http://stackoverflow.com/questions/29348742/spring-data-jpa-creating-specification-query-fetch-joins
        // root.fetch("aliases", JoinType.LEFT);
        // root.fetch("attributes", JoinType.LEFT);
        // query.distinct(true);
        // query.orderBy(cb.asc(root.get("entrezGeneId")));
        // return cb.equal(root.get("symbol"), symbol);
        return criteriaBuilder.lessThanOrEqualTo(root.get(condition.getField()), Integer.valueOf(condition.getValue().toString()));
    }

    private Predicate buildPredicateToCriteriaLe(SpecConVo condition, Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        return criteriaBuilder.lessThanOrEqualTo(root.get(condition.getField()), Integer.valueOf(condition.getValue().toString()));
    }

    private Predicate buildPredicateToCriteriaLk(SpecConVo condition, Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        return criteriaBuilder.like(root.get(condition.getField()), condition.getValue().toString().trim());
    }

    private Predicate buildPredicateToCriteriaIn(SpecConVo condition, Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        Predicate predicate = root.get(condition.getField()).in(condition.getValue());
        return predicate; // http://4byte.cn/question/145956/difference-between-criteriabuilder-and-criteriaquery-for-a-collection.html
    }

    private Predicate buildPredicateToCriteriaEq(SpecConVo condition, Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        return criteriaBuilder.equal(root.get(condition.getField()), condition.getValue());
    }

    private Predicate buildPredicateToCriteriaGe(SpecConVo condition, Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        return criteriaBuilder.greaterThanOrEqualTo(root.get(condition.getField()), Integer.valueOf(condition.getValue().toString().trim()));
    }

    private Predicate buildPredicateToCriteriaGt(SpecConVo condition, Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
        ParameterExpression<Date> fromDate1 = criteriaBuilder.parameter(Date.class);
        return criteriaBuilder.greaterThan(root.get(condition.getField()), condition.getValue().toString().trim());
    }

    /**
     * get #{bare_field_comment} .
     * 
     * @return the specificationVo
     */
    public SpecVo getSpecificationVo() {
        return specificationVo;
    }

    /**
     * set #{bare_field_comment}.
     * 
     * @param specificationVo
     *            the specificationVo to set
     */
    public void setSpecificationVo(SpecVo specificationVo) {
        this.specificationVo = specificationVo;
    }

}
